//
// Created by yangentao on 2021/1/30.
//

import Foundation
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif

public extension View {
    @discardableResult
    func addViewConstraints<T: View>(_ child: T, _ block: (ConstraintBuilder) -> Void) -> T {
        self.addSubview(child)
        child.constraints(block)
        return child
    }
}

public extension View {

    @discardableResult
    func constraints(_ block: (ConstraintBuilder) -> Void) -> Self {
        let b = ConstraintBuilder(self)
        block(b)
        for item in b.items {
            self.constraintParams[item.key] = item
            if item.ident == nil {
                item.ident = "LayoutIdent(name=\(self.name ?? "") layoutAttr=\(item.attr.rawValue) view=\(type(of: self))  )"
            }
        }
        if self.superview != nil {
            constraintParams.installAll(self)
        } else {
            loge("superview is nil, you should call constraintParams.installAll()  manualy ")
        }
        return self
    }


    internal var constraintParams: ConstraintParams {
        ensureAttr(key: "constraintParams") {
            ConstraintParams()
        }
    }
}

public class ConstraintParams {
    public var items: Map<ConstraintKey, ConstraintItem> = Map()


    internal func installAll(_ view: View) {
        for (_, item) in items {
            item.install(view)
        }
        var b = false
        if let w = items[ConstraintKey(attr: .width, rel: .equal)], w.wrap {
            b = true
        }
        if let h = items[ConstraintKey(attr: .height, rel: .equal)], h.wrap {
            b = true
        }
        if b {
            view.needLayoutOnPropChanged()
        }
    }

    subscript(key: ConstraintKey) -> ConstraintItem? {
        get {
            items[key]
        }
        set {
            items[key] = newValue
        }
    }
}


public class ConstraintItem {
    var attr: LayoutAttribute
    var relation: LayoutRelation = .equal
    weak var otherView: View? = nil
    var otherName: String? = nil
    var otherAttr: LayoutAttribute = .notAnAttribute
    var multiplier: CGFloat = 1
    var constant: CGFloat = 0
    var ident: String? = nil
    var priority: LayoutPriority = .required
    var wrap: Bool = false

    init(attr: LayoutAttribute) {
        self.attr = attr
    }

    var key: ConstraintKey {
        ConstraintKey(attr: self.attr, rel: self.relation)
    }


    public func install(_ view: View) {
        var otherProp: ConstraintAttr = otherAttr
        let toItem = otherView ?? view.find(otherView: otherName)
        if toItem != nil {
            if otherAttr == .notAnAttribute {
                otherProp = attr
            }
        }

        let cp = NSLayoutConstraint(item: view as Any, attribute: attr, relatedBy: relation, toItem: toItem, attribute: otherProp, multiplier: multiplier, constant: constant)
        cp.priority = priority
        cp.identifier = ident
        view.constraintStorage.put(cp)

    }

    @discardableResult
    public func ident(_ id: String) -> Self {
        ident = id
        return self
    }

    @discardableResult
    public func priority(_ p: LayoutPriority) -> Self {
        priority = p
        return self
    }

    @discardableResult
    public func priority(_ p: Float) -> Self {
        priority = LayoutPriority(rawValue: p)
        return self
    }

    @discardableResult
    func other(_ otherView: OtherView?) -> Self {
        if let s = otherView as? String {
            self.otherName = s
        } else if let v = otherView as? View {
            self.otherView = v
        }
        return self
    }

    @discardableResult
    func wrap(_ b: Bool) -> Self {
        self.wrap = b
        return self
    }

    @discardableResult
    fileprivate func relTo(rel: LayoutRelation, otherView: OtherView? = nil, otherAttr: LayoutAttribute = .notAnAttribute, multi: CGFloat = 1, constant: CGFloat = 0) -> Self {
        self.relation = rel
        self.multiplier = multi
        self.constant = constant
        self.other(otherView)
        self.otherAttr = otherAttr

        return self
    }

}

public extension ConstraintBuilder {

    func widthWrap() {
        let sz = self.view.fitSize(.zero)
        self.width.eq(sz.width).priority(.defaultHigh).wrap(true)
    }

    func heightWrap() {
        let sz = self.view.fitSize(.zero)
        self.height.eq(sz.height).priority(.defaultHigh).wrap(true)
    }

}


public class ConstraintBuilder {
    let items: List<ConstraintItem> = List()

    public unowned let view: View

    fileprivate init(_ view: View) {
        self.view = view
        view.translatesAutoresizingMaskIntoConstraints = false
    }

    public func heightWrap(minValue: CGFloat) {
        self.height.ge(minValue).priority(.defaultHigh)
        view.keepContent(.vertical)
    }
}

public extension ConstraintBuilder {

    @discardableResult
    func edgesParent() -> Self {
        edges.eqParent()
        return self
    }

    @discardableResult
    func ratio(_ multi: CGFloat) -> ConstraintBuilderEnd {
        width.eq(self: .height).multi(multi)
    }

    @discardableResult
    func toLeftOf(_ otherView: OtherView) -> ConstraintBuilderEnd {
        right.eq(otherView, .left)
    }

    @discardableResult
    func toRightOf(_ otherView: OtherView) -> ConstraintBuilderEnd {
        left.eq(otherView, .right)
    }

    @discardableResult
    func below(_ otherView: OtherView) -> ConstraintBuilderEnd {
        top.eq(otherView, .bottom)
    }

    @discardableResult
    func above(_ otherView: OtherView) -> ConstraintBuilderEnd {
        bottom.eq(otherView, .top)
    }

}


public extension ConstraintBuilder {
    private func props(_ attrs: LayoutAttribute...) -> ConstraintBuilderEnd {
        let ls = attrs.map {
            ConstraintItem(attr: $0)
        }
        self.items.append(contentsOf: ls)
        return ConstraintBuilderEnd(ls)
    }

    var edges: ConstraintBuilderEnd {
        props(.left, .top, .right, .bottom)
    }
    var edgeX: ConstraintBuilderEnd {
        props(.left, .right)
    }
    var edgeY: ConstraintBuilderEnd {
        props(.top, .bottom)
    }

    var leftTop: ConstraintBuilderEnd {
        props(.left, .top)
    }
    var leftBottom: ConstraintBuilderEnd {
        props(.left, .bottom)
    }
    var rightTop: ConstraintBuilderEnd {
        props(.right, .top)
    }
    var rightBottom: ConstraintBuilderEnd {
        props(.right, .bottom)
    }
    var size: ConstraintBuilderEnd {
        props(.width, .height)
    }
    var center: ConstraintBuilderEnd {
        props(.centerX, .centerY)
    }

    //------
    var left: ConstraintBuilderEnd {
        props(.left)
    }
    var right: ConstraintBuilderEnd {
        props(.right)
    }
    var top: ConstraintBuilderEnd {
        props(.top)
    }
    var bottom: ConstraintBuilderEnd {
        props(.bottom)
    }
    var centerX: ConstraintBuilderEnd {
        props(.centerX)
    }
    var centerY: ConstraintBuilderEnd {
        props(.centerY)
    }
    var width: ConstraintBuilderEnd {
        props(.width)
    }
    var height: ConstraintBuilderEnd {
        props(.height)
    }
    var leading: ConstraintBuilderEnd {
        props(.leading)
    }
    var trailing: ConstraintBuilderEnd {
        props(.trailing)
    }

    var lastBaseline: ConstraintBuilderEnd {
        props(.lastBaseline)
    }
    var firstBaseline: ConstraintBuilderEnd {
        props(.firstBaseline)
    }

    #if os(iOS)
    var leftMargin: ConstraintBuilderEnd {
        props(.leftMargin)
    }
    var rightMargin: ConstraintBuilderEnd {
        props(.rightMargin)
    }
    var topMargin: ConstraintBuilderEnd {
        props(.topMargin)
    }
    var bottomMargin: ConstraintBuilderEnd {
        props(.bottomMargin)
    }
    var leadingMargin: ConstraintBuilderEnd {
        props(.leadingMargin)
    }
    var trailingMargin: ConstraintBuilderEnd {
        props(.trailingMargin)
    }
    var centerXWithinMargins: ConstraintBuilderEnd {
        props(.centerXWithinMargins)
    }
    var centerYWithinMargins: ConstraintBuilderEnd {
        props(.centerYWithinMargins)
    }
    #endif
}

public class ConstraintBuilderEnd {
    fileprivate var items: [ConstraintItem]

    fileprivate init(_ items: [ConstraintItem]) {
        self.items = items
    }


    @discardableResult
    public func ident(_ id: String) -> Self {
        items.each {
            $0.ident = id
        }
        return self
    }

    @discardableResult
    public func priority(_ p: LayoutPriority) -> Self {
        items.each {
            $0.priority = p
        }
        return self
    }

    @discardableResult
    public func wrap(_ b: Bool) -> Self {
        items.each {
            $0.wrap = b
        }
        return self
    }

    @discardableResult
    public func priority(_ p: Float) -> Self {
        priority(LayoutPriority(rawValue: p))
    }

    @discardableResult
    public func constant(_ c: CGFloat) -> Self {
        items.each {
            $0.constant = c
        }
        return self
    }

    @discardableResult
    public func multi(_ m: CGFloat) -> Self {
        items.each {
            $0.multiplier = m
        }
        return self
    }

    @discardableResult
    public func inset(_ leftTop: CGFloat, _ rightBottom: CGFloat) -> Self {
        for item in items {
            switch item.attr {
            case .left, .top, .leading, .centerX, .centerY, .leftMargin, .topMargin, .leadingMargin, .centerXWithinMargins, .centerYWithinMargins:
                item.constant = leftTop
            case .right, .bottom, .trailing, .width, .height, .rightMargin, .bottomMargin, .trailingMargin:
                item.constant = -rightBottom
            default:
                fatalError("unknow attribute with inset invoke, you maybe want use constant() instead!")
            }
        }
        return self
    }

    @discardableResult
    public func inset(_ c: CGFloat) -> Self {
        inset(c, c)
    }
}

public extension ConstraintBuilderEnd {


    @discardableResult
    private func relTo(rel: LayoutRelation, otherView: OtherView?, otherAttr: LayoutAttribute = .notAnAttribute, multi: CGFloat = 1, constant: CGFloat = 0) -> Self {
        for a in self.items {
            a.relTo(rel: rel, otherView: otherView, otherAttr: otherAttr, multi: multi, constant: constant)
        }
        return self
    }


    @discardableResult
    private func relConst(rel: LayoutRelation, constant: CGFloat = 0) -> Self {
        for a in self.items {
            a.relTo(rel: rel, otherView: nil, otherAttr: .notAnAttribute, multi: 1, constant: constant)
        }
        return self
    }

    @discardableResult
    func eq(_ otherView: OtherView, _ otherAttr: LayoutAttribute = .notAnAttribute) -> Self {
        relTo(rel: .equal, otherView: otherView, otherAttr: otherAttr)
    }

    @discardableResult
    func ge(_ otherView: OtherView, _ otherAttr: LayoutAttribute = .notAnAttribute) -> Self {
        relTo(rel: .greaterThanOrEqual, otherView: otherView, otherAttr: otherAttr)
    }

    @discardableResult
    func le(_ otherView: OtherView, _  otherAttr: LayoutAttribute = .notAnAttribute) -> Self {
        relTo(rel: .lessThanOrEqual, otherView: otherView, otherAttr: otherAttr)
    }


    @discardableResult
    func eqParent() -> Self {
        relTo(rel: .equal, otherView: "..", otherAttr: .notAnAttribute)
    }

    @discardableResult
    func geParent() -> Self {
        relTo(rel: .greaterThanOrEqual, otherView: "..", otherAttr: .notAnAttribute)
    }

    @discardableResult
    func leParent() -> Self {
        relTo(rel: .lessThanOrEqual, otherView: "..", otherAttr: .notAnAttribute)
    }

    @discardableResult
    func ge(parent otherAttr: LayoutAttribute) -> Self {
        relTo(rel: .greaterThanOrEqual, otherView: "..", otherAttr: otherAttr)
    }

    @discardableResult
    func le(parent otherAttr: LayoutAttribute) -> Self {
        relTo(rel: .lessThanOrEqual, otherView: "..", otherAttr: otherAttr)
    }

    @discardableResult
    func eq(parent otherAttr: LayoutAttribute) -> Self {
        relTo(rel: .equal, otherView: "..", otherAttr: otherAttr)
    }

    @discardableResult
    func eq(self otherAttr: LayoutAttribute) -> Self {
        relTo(rel: .equal, otherView: ".", otherAttr: otherAttr)
    }


    @discardableResult
    func ge(self otherAttr: LayoutAttribute) -> Self {
        relTo(rel: .greaterThanOrEqual, otherView: ".", otherAttr: otherAttr)
    }

    @discardableResult
    func le(self otherAttr: LayoutAttribute) -> Self {
        relTo(rel: .lessThanOrEqual, otherView: ".", otherAttr: otherAttr)
    }

    @discardableResult
    func eq(_ constant: CGFloat) -> Self {
        relTo(rel: .equal, otherView: nil, constant: constant)
    }

    @discardableResult
    func ge(_ constant: CGFloat) -> Self {
        relTo(rel: .greaterThanOrEqual, otherView: nil, constant: constant)
    }

    @discardableResult
    func le(_ constant: CGFloat) -> Self {
        relTo(rel: .lessThanOrEqual, otherView: nil, constant: constant)
    }


    @discardableResult
    func eq(_ size: Size) -> Self {
        for item in items {
            item.relation = .equal
            if item.attr == .width {
                item.constant = size.width
            } else if item.attr == .height {
                item.constant = size.height
            }
        }
        return self
    }

}



